/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.shared;

import java.util.HashMap;
import java.util.List;

import org.bmi.gwt.mi.shared.callbacks.AttributeCallback;
import org.bmi.gwt.mi.shared.callbacks.BindCallback;
import org.bmi.gwt.mi.shared.communication.attribute.AttributeValue;
import org.bmi.gwt.mi.shared.communication.call.Call;
import org.bmi.gwt.mi.shared.communication.call.ConstructorCall;
import org.bmi.gwt.mi.shared.communication.call.MethodCall;
import org.bmi.gwt.mi.shared.communication.call.TrCommitCall;
import org.bmi.gwt.mi.shared.communication.call.TrConstructorCall;
import org.bmi.gwt.mi.shared.communication.call.TrFactoryCall;
import org.bmi.gwt.mi.shared.communication.call.TrMethodCall;
import org.bmi.gwt.mi.shared.communication.ptypes.Param;
import org.bmi.gwt.mi.shared.util.ObjectWrapper;
import org.bmi.gwt.mi.shared.util.WrapperFactory;

/**
 * A channel provides the access to available queue's functionalities.
 * 
 * A queue can be created or reserved by an user, by providing valid
 * certificate; a queue can be reserved only in case it has no defined owner.
 * Once a queue has defined an owner only he/she can bind it, listening for
 * method invocations.
 * 
 * @author marco.tamburelli@gmail.com
 */
public abstract class Channel extends BroadChannel
{
	protected WrapperFactory wrapperFactory;

	private HashMap<String, ObjectWrapper> objectMap;
	private HashMap<String, HashMap<String, Object>> transactionsTable;

	protected HashMap<String, AttributeValue<?>> attributes;

	/*
	 * There are 2 flags bind and bindAllowed: 1. The value of "bound" is set
	 * after attempting to bind to a queue, and is true in case of success 2.
	 * The value of "bindAllowed" at the beginning is true, but in case of a
	 * failure while attempting to bing to a queue it is set to false.
	 */

	protected boolean bound;
	protected boolean bindAllowed;

	protected Channel()
	{
		this.objectMap = new HashMap<String, ObjectWrapper>();
		this.transactionsTable = new HashMap<String, HashMap<String, Object>>();

		this.attributes = new HashMap<String, AttributeValue<?>>();

		this.bound = false;
		this.bindAllowed = true;
	}

	/**
	 * Register an object to its id, and listen for its method invocation.
	 * 
	 * In case the bind is not allowed (see {@link #isBindAllowed()}) this
	 * method has no effect.
	 * 
	 * @param objectId The designed id of a provided object. An object can be
	 *        registered only if it implements {@link MiObject}.
	 * @param obj An object.
	 */
	public void register(String objectId, MiObject obj)
	{
		ObjectWrapper wrapper = this.wrapperFactory.createWrapper(obj);

		this.objectMap.put(objectId, wrapper);
	}

	/**
	 * Unregisters an object previously registered with the provided id.
	 * 
	 * @param objectId The designed id of a provided object.
	 */
	public void unregister(String objectId)
	{
		this.objectMap.remove(objectId);
	}

	/**
	 * Listens for method invocations, and executes any method of already
	 * registered or created objects.
	 * 
	 * In case the bind is not allowed (see {@link #isBindAllowed()}) this
	 * method has no effect.
	 * 
	 * @param callback The callback which will be used after the bind attempt.
	 */
	public abstract void bind(BindCallback callback);

	/**
	 * This method unbind the queue, stopping to poll, and if the "discard" flag
	 * is set to <code>true</code> the queue will be discarder.
	 * 
	 * @param discard When set to <code>true</code> the queue will be discarded
	 *        firing the proper event on server. A queue will be hoverer
	 *        discarded after a short time of inactivity.
	 */
	public abstract void unbind(boolean discard);

	/**
	 * This method unbind the queue, stopping to poll.
	 * 
	 * Note that such method does't provoke the queue to be discarded
	 * immediately; the queue will be discarded after a short time of
	 * inactivity.
	 */
	public void unbind()
	{
		unbind(false);
	}

	/**
	 * Returns <code>true</code> when the queue is binding or already bound.
	 * 
	 * @return
	 */
	public boolean isBound()
	{
		return this.bound;
	}

	/**
	 * Returns <code>true</code> if the queue allows binding. In the other case
	 * it means that the queue is owned by another client.
	 * 
	 * @return
	 */
	public boolean isBindAllowed()
	{
		return bindAllowed;
	}

	protected abstract void onResponse(List<Call> calls) throws Exception;

	protected void manageResponse(List<Call> calls) throws Exception
	{
		for (Call call : calls)
			if (call instanceof TrConstructorCall)
			{
				TrConstructorCall _c = (TrConstructorCall) call;
				String transactionId = _c.getTransactionId();

				getReturnValuesMap(transactionId);

				ObjectWrapper wrapper = this.wrapperFactory.createWrapper(_c.getTargetClassName(), _c
						.getArgsSignature(), getReturnValuesMap(transactionId), this.objectMap, _c.getConstructorArgs()
						.toArray(new Param<?>[0]));

				this.objectMap.put(_c.getObjectId(), wrapper);
			}
			else if (call instanceof ConstructorCall)
			{
				ConstructorCall _c = (ConstructorCall) call;

				ObjectWrapper wrapper = this.wrapperFactory.createWrapper(_c.getTargetClassName(),
						_c.getArgsSignature(), this.objectMap, _c.getConstructorArgs().toArray(new Param<?>[0]));

				this.objectMap.put(_c.getObjectId(), wrapper);
			}
			else if (call instanceof TrCommitCall)
				this.transactionsTable.remove(((TrCommitCall) call).getTransactionId());
			else
			{
				String objectId = call.getObjectId();

				/*
				 * If the object with such Id is not yet registered, nothing
				 * will happen.
				 */
				if (this.objectMap.containsKey(objectId))
				{
					if (call instanceof TrMethodCall)
					{
						TrMethodCall _c = (TrMethodCall) call;
						String transactionId = _c.getTransactionId();

						ObjectWrapper wrapper = this.objectMap.get(objectId);
						Object returnValue = wrapper.invoke(_c.getMethod(), _c.getArgsSignature(),
								getReturnValuesMap(transactionId), this.objectMap,
								_c.getArguments().toArray(new Param<?>[0]));
						getReturnValuesMap(transactionId).put(_c.getReturnId(), returnValue);
					}
					else if (call instanceof TrFactoryCall)
					{
						TrFactoryCall _c = (TrFactoryCall) call;
						String transactionId = _c.getTransactionId();

						ObjectWrapper factoryWrapper = this.objectMap.get(objectId);

						MiObject obj = (MiObject) factoryWrapper.invoke(_c.getFactoryMethod(), _c.getArgsSignature(),
								getReturnValuesMap(transactionId), this.objectMap,
								_c.getFactoryMethodArgs().toArray(new Param<?>[0]));

						ObjectWrapper productWrapper = this.wrapperFactory.createWrapper(obj);

						this.objectMap.put(_c.getCreatedObjectId(), productWrapper);
					}
					else if (call instanceof MethodCall)
					{
						MethodCall _c = (MethodCall) call;

						ObjectWrapper wrapper = this.objectMap.get(objectId);
						wrapper.invoke(_c.getMethod(), _c.getArgsSignature(), objectMap,
								_c.getArguments().toArray(new Param<?>[0]));
					}
				}
			}
	}

	private HashMap<String, Object> getReturnValuesMap(String transactionId)
	{
		if (!this.transactionsTable.containsKey(transactionId))
			this.transactionsTable.put(transactionId, new HashMap<String, Object>());

		return this.transactionsTable.get(transactionId);
	}

	/**
	 * Returns through the provided callback the value of an attribute
	 * identified by a key.
	 * 
	 * @param key The attribute key.
	 * @param callback The callback function which invokes
	 *        {@link AttributeCallback#onSuccess(AttributeValue)} providing the
	 *        requested value..
	 */
	public abstract <T extends AttributeValue<?>> void getAttribute(String key, AttributeCallback<T> callback);

	/**
	 * Attempts to insert or update the attribute using provided key, and
	 * provided value.
	 * 
	 * @param key The attribute key.
	 * @param value the value of the attribute.
	 * @param callback The callback function which invokes
	 *        {@link AttributeCallback#onSuccess(AttributeValue)} providing the
	 *        new provided value.
	 */
	public abstract <T extends AttributeValue<?>> void setAttribute(String key, T value, AttributeCallback<T> callback);

	/**
	 * Attempts to remove the attribute with the provided key.
	 * 
	 * @param key The attribute key.
	 * @param callback The callback function which invokes
	 *        {@link AttributeCallback#onSuccess(AttributeValue)} providing the
	 *        removed value.
	 */
	public abstract <T extends AttributeValue<?>> void removeAttribute(String key, AttributeCallback<T> callback);
}
